﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace ComputedTable.Version10
{
    public class Table<TRowId, TColumnId, TOperationContext> : ITable<TRowId, TColumnId>
        where TOperationContext : IOperationContext<TRowId, TColumnId>
    {
        private Cell<TRowId, TColumnId>[,] cells;
        private ICalculatorFactory<TRowId, TColumnId, TOperationContext> calculatorFactory;

        private IRowResolver<TRowId> rowResolver;
        private IColumnResolver<TColumnId> colResolver;

        public Table(
            IRowResolver<TRowId> rowResolver,
            IColumnResolver<TColumnId> columnResolver,
            TOperationContext operationContext,
            ICalculatorFactory<TRowId, TColumnId, TOperationContext> calculatorFactory = null)
        {
            this.rowResolver = rowResolver;
            this.colResolver = columnResolver;
            this.OperationContext = operationContext;
            this.OperationContext.Table = this;

            this.RowIds = this.rowResolver.Ids;
            this.RowCount = this.rowResolver.Count;
            this.ColCount = this.colResolver.Ids.Count;
            this.ColumnIds = this.colResolver.Ids;

            this.calculatorFactory = calculatorFactory;
            this.InitializeCells();
        }

        public event EventHandler<TableEventArgs<TRowId, TColumnId, TOperationContext>> AfterCellValueComputed;
        public event EventHandler<TableEventArgs<TRowId, TColumnId, TOperationContext>> BeforeCellValueComputed;
        public event EventHandler<TableEventArgs<TRowId, TColumnId, TOperationContext>> CellCalculationError;

        public TOperationContext OperationContext { get; private set; }
        
        public int RowCount { get; private set; }

        public int ColCount { get; private set; }

        public IList<TColumnId> ColumnIds { get; private set; }

        public IList<TRowId> RowIds { get; private set; }

        public Cell<TRowId, TColumnId> this[TRowId row, TColumnId col]
        {
            get
            {
                int rowIndex = this.rowResolver.IndexOf(row);
                int colIndex = this.colResolver.IndexOf(col);
                return this.cells[rowIndex, colIndex];
            }
        }

        public Cell<TRowId, TColumnId> Up(Cell<TRowId, TColumnId> cell)
        {
            int rowIndex = this.rowResolver.IndexOfPrevious(cell.RowId);
            if (rowIndex == -1)
            {
                return null;
            }

            int colIndex = this.colResolver.IndexOf(cell.ColumnId);
            return this.cells[rowIndex, colIndex];
        }

        public void SetColumn(TColumnId column, object value)
        {
            int colIndex = this.colResolver.IndexOf(column);

            for (int rowIndex = 0; rowIndex < this.rowResolver.Count; rowIndex++)
            {
                var cell = this.cells[rowIndex, colIndex];
                cell.Value = value;
                cell.Formula = null;
            }
        }

        public void SetColumnWithFormula(TColumnId column, Func<TOperationContext, CellOperationResult> formula)
        {
            int colIndex = this.colResolver.IndexOf(column);

            for (int rowIndex = 0; rowIndex < this.rowResolver.Count; rowIndex++)
            {
                var cell = this.cells[rowIndex, colIndex];
                cell.Value = null;
                cell.Formula = () => { return formula(this.OperationContext); };
            }
        }

        public void SetColumnWithCalculatorFromFactory<TCalculator>(TColumnId column) where TCalculator : ICalculator<TOperationContext>
        {
            if (this.calculatorFactory == null)
            {
                throw new InvalidOperationException("CalculatorFactory must be passed in the constructor");
            }

            var calculator = this.calculatorFactory.Resolve<TCalculator>();
            this.SetColumnWithCalculator(column, calculator);
        }

        public void SetColumnWithCalculator<TCalculator>(TColumnId column) where TCalculator : ICalculator<TOperationContext>, new()
        {
            var calculator = new TCalculator();
            this.SetColumnWithCalculator(column, calculator);
        }

        public bool Compute()
        {
            for (int rowIndex = 0; rowIndex < this.rowResolver.Count; rowIndex++)
            {
                for (int colIndex = 0; colIndex < this.colResolver.Ids.Count; colIndex++)
                {
                    this.OperationContext.CurrentCell = this.cells[rowIndex, colIndex];
                    TableEventArgs<TRowId, TColumnId, TOperationContext> e = new TableEventArgs<TRowId, TColumnId, TOperationContext>(this.OperationContext);
                    e.CanContinue = true;

                    this.OnBeforeCellValueComputed(e);
                    if (!e.CanContinue)
                    {
                        this.OperationContext.LastOperationResult = CellOperationResult.CreatedFailed("Calculation stop requested from BeforeCellValueComputed event handler.");
                        return false;
                    }

                    this.OperationContext.LastOperationResult = this.OperationContext.CurrentCell.Compute();

                    e.CanContinue = this.OperationContext.LastOperationResult.Succeed;
                    if (!e.CanContinue)
                    {
                        this.OnCellCalculationError(e);
                    }

                    this.OnAfterCellValueComputed(e);
                    if (!e.CanContinue)
                    {
                        if (this.OperationContext.LastOperationResult.Succeed)
                        {
                            this.OperationContext.LastOperationResult = CellOperationResult.CreatedFailed("Calculation stop requested from AfterCellValueComputed event handler.");
                        }

                        return false;
                    }
                }
            }

            return true;
        }

        public void SaveToCsv(string path, bool includeHeaders = true, TColumnId[] columns = null)
        {
            List<TColumnId> colList;
            if (columns == null)
            {
                colList = this.colResolver.Ids.ToList();
            }
            else
            {
                colList = columns.ToList();
            }

            StringBuilder sb = new StringBuilder();
            string line;
            if (includeHeaders)
            {
                line = string.Join(",", colList.Select(a => a.ToString()));
                sb.AppendLine(line);
            }

            foreach (var row in this.rowResolver.Ids)
            {
                var values = new List<string>();
                foreach (var column in colList)
                {
                    var cell = this[row, column];
                    values.Add(cell.Value == null ? string.Empty : cell.Value.ToString());
                }

                line = string.Join(",", values);
                sb.AppendLine(line);
            }

            File.WriteAllText(path, sb.ToString());
        }

        public void SaveToXml(string path, string nodeName = "table", string itemName = "row")
        {
            var xml = new XElement(nodeName);

            this.TraverseRows((rowIndex, cells) =>
            {
                var row = new XElement(itemName);
                foreach (var cell in cells)
                {
                    var attribute = new XAttribute(
                        cell.ColumnId.ToString().Replace(" ", "_"),
                        cell.Value == null ? string.Empty : cell.Value.ToString());

                    row.Add(attribute);
                }

                xml.Add(row);
            });

            File.WriteAllText(path, xml.ToString());
        }

        public List<Row> Rows
        {
            get
            {
                var result = new List<Row>();
                this.TraverseRows((rowIndex, cells) =>
                {
                    var row = new Row(rowIndex, cells, this.colResolver);
                    result.Add(row);
                });

                return result;
            }
        }

        protected virtual void OnAfterCellValueComputed(TableEventArgs<TRowId, TColumnId, TOperationContext> e)
        {
            this.RaiseEvent(this.AfterCellValueComputed, e);
        }

        protected virtual void OnBeforeCellValueComputed(TableEventArgs<TRowId, TColumnId, TOperationContext> e)
        {
            this.RaiseEvent(this.BeforeCellValueComputed, e);
        }

        protected virtual void OnCellCalculationError(TableEventArgs<TRowId, TColumnId, TOperationContext> e)
        {
            this.RaiseEvent(this.CellCalculationError, e);
        }

        private void RaiseEvent(
            EventHandler<TableEventArgs<TRowId, TColumnId, TOperationContext>> handler,
            TableEventArgs<TRowId, TColumnId, TOperationContext> e)
        {
            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void InitializeCells()
        {
            this.cells = new Cell<TRowId, TColumnId>[this.rowResolver.Count, this.colResolver.Ids.Count];

            int rowIndex;
            int colIndex;

            foreach (var row in this.rowResolver.Ids)
            {
                rowIndex = this.rowResolver.IndexOf(row);
                foreach (var col in this.colResolver.Ids)
                {
                    colIndex = this.colResolver.IndexOf(col);
                    this.cells[rowIndex, colIndex] = new Cell<TRowId, TColumnId>(row, col);
                }
            }
        }

        private List<Cell<TRowId, TColumnId>> GetCellsByRow(int rowIndex)
        {
            var result = new List<Cell<TRowId, TColumnId>>();
            for (int colIndex = 0; colIndex < this.colResolver.Ids.Count; colIndex++)
            {
                var cell = this.cells[rowIndex, colIndex];
                result.Add(cell);
            }

            return result;
        }

        private void TraverseTable(Action<Cell<TRowId, TColumnId>> action)
        {
            for (int rowIndex = 0; rowIndex < this.rowResolver.Count; rowIndex++)
            {
                for (int colIndex = 0; colIndex < this.colResolver.Count; colIndex++)
                {
                    var cell = this.cells[rowIndex, colIndex];
                    action(cell);
                }
            }
        }

        private void TraverseRows(Action<int, List<Cell<TRowId, TColumnId>>> action)
        {
            for (int rowIndex = 0; rowIndex < this.rowResolver.Count; rowIndex++)
            {
                var cells = this.GetCellsByRow(rowIndex);
                action(rowIndex, cells);
            }
        }

        private void TraverseColumn(int colIndex, Action<Cell<TRowId, TColumnId>> action)
        {
            for (int rowIndex = 0; rowIndex < this.rowResolver.Count; rowIndex++)
            {
                var cell = this.cells[rowIndex, colIndex];
                action(cell);
            }
        }

        private void SetColumnWithCalculator(TColumnId column, ICalculator<TOperationContext> calculator)
        {
            int colIndex = this.colResolver.IndexOf(column);

            this.TraverseColumn(
                colIndex,
                (cell) =>
                {
                    cell.Value = null;
                    cell.Formula = () => { return calculator.Execute(this.OperationContext); };
                });
        }

        public class Row
        {
            private IColumnResolver<TColumnId> colResolver;
            private IList<Cell<TRowId, TColumnId>> cells;

            public int Index { get; private set; }

            internal Row(int index, IList<Cell<TRowId, TColumnId>> cells, IColumnResolver<TColumnId> colResolver)
            {
                this.Index = index;
                this.cells = cells;
                this.colResolver = colResolver;
            }

            public Cell<TRowId, TColumnId> GetCellByColumn(TColumnId columnId)
            {
                int colIndex = this.colResolver.IndexOf(columnId);
                if (colIndex == -1)
                {
                    throw new Exception("Column not found");
                }

                return this.cells[colIndex];
            }
        }
    }
}
